package eval

import (
	"fmt"
	"os"
	"showen/lex"
	"showen/obj"
	"showen/parse"
	"showen/tok"
)

// Global variables to track transcript state
var (
	transcriptFile   *os.File
	transcriptActive bool
)

// InputPortP implements (input-port? obj)
func InputPortP(args []obj.Object, env *obj.Environment) obj.Object {
	if len(args) != 1 {
		return &Error{Message: "input-port? requires exactly 1 argument"}
	}

	if args[0].Type() != obj.PORT_OBJ {
		return &obj.BooleanObject{Value: false}
	}

	port := args[0].(*obj.PortObject)
	return &obj.BooleanObject{Value: port.IsInput}
}

// OutputPortP implements (output-port? obj)
func OutputPortP(args []obj.Object, env *obj.Environment) obj.Object {
	if len(args) != 1 {
		return &Error{Message: "output-port? requires exactly 1 argument"}
	}

	if args[0].Type() != obj.PORT_OBJ {
		return &obj.BooleanObject{Value: false}
	}

	port := args[0].(*obj.PortObject)
	return &obj.BooleanObject{Value: !port.IsInput}
}

// CurrentInputPort implements (current-input-port)
func CurrentInputPort(args []obj.Object, env *obj.Environment) obj.Object {
	if len(args) != 0 {
		return &Error{Message: "current-input-port takes no arguments"}
	}

	// In a full implementation, we would return the actual current input port
	// For now, we'll return a dummy input port
	return &obj.PortObject{
		File:    "*stdin*",
		IsOpen:  true,
		IsInput: true,
	}
}

// CurrentOutputPort implements (current-output-port)
func CurrentOutputPort(args []obj.Object, env *obj.Environment) obj.Object {
	if len(args) != 0 {
		return &Error{Message: "current-output-port takes no arguments"}
	}

	// In a full implementation, we would return the actual current output port
	// For now, we'll return a dummy output port
	return &obj.PortObject{
		File:    "*stdout*",
		IsOpen:  true,
		IsInput: false,
	}
}

// CharReadyP implements (char-ready?) and (char-ready? port)
func CharReadyP(args []obj.Object, env *obj.Environment) obj.Object {
	// According to R4RS, char-ready? takes an optional port argument
	if len(args) > 1 {
		return &Error{Message: "char-ready? takes at most 1 argument"}
	}

	// Check if a port is provided and validate it
	if len(args) == 1 {
		if args[0].Type() != obj.PORT_OBJ {
			return &Error{Message: "char-ready?: port argument must be a port object"}
		}

		port := args[0].(*obj.PortObject)
		if !port.IsOpen {
			return &Error{Message: "char-ready?: port is not open"}
		}

		if !port.IsInput {
			return &Error{Message: "char-ready?: port is not an input port"}
		}
	}

	// In a full implementation:
	// 1. Get the port (default to current input port if none provided)
	// 2. Check if a character is ready to be read without blocking

	// For now, we'll always return true (characters are always ready in our simplified implementation)
	return &obj.BooleanObject{Value: true}
}

// CallWithInputFile implements (call-with-input-file string proc)
func CallWithInputFile(args []obj.Object, env *obj.Environment) obj.Object {
	if len(args) != 2 {
		return &Error{Message: "call-with-input-file requires exactly 2 arguments"}
	}

	if args[0].Type() != obj.STRING_OBJ {
		return &Error{Message: "call-with-input-file: first argument must be a string"}
	}

	// Check if the second argument is a procedure
	procType := args[1].Type()
	if procType != obj.PROCEDURE_OBJ && procType != obj.BUILTIN_OBJ && procType != obj.LAMBDA_OBJ {
		return &Error{Message: "call-with-input-file: second argument must be a procedure"}
	}

	filename := args[0].(*obj.StringObject).Value

	// Open the input file
	port := &obj.PortObject{
		File:    filename,
		IsOpen:  true,
		IsInput: true,
	}

	// Call the procedure with the port as argument
	result, err := applyObjectProcedure(args[1], []obj.Object{port}, env)
	if err != nil {
		// Close the port before returning the error
		port.IsOpen = false
		return &Error{Message: err.Error()}
	}

	// Close the port
	port.IsOpen = false

	return result
}

// CallWithOutputFile implements (call-with-output-file string proc)
func CallWithOutputFile(args []obj.Object, env *obj.Environment) obj.Object {
	if len(args) != 2 {
		return &Error{Message: "call-with-output-file requires exactly 2 arguments"}
	}

	if args[0].Type() != obj.STRING_OBJ {
		return &Error{Message: "call-with-output-file: first argument must be a string"}
	}

	// Check if the second argument is a procedure
	procType := args[1].Type()
	if procType != obj.PROCEDURE_OBJ && procType != obj.BUILTIN_OBJ && procType != obj.LAMBDA_OBJ {
		return &Error{Message: "call-with-output-file: second argument must be a procedure"}
	}

	filename := args[0].(*obj.StringObject).Value

	// Open the output file
	port := &obj.PortObject{
		File:    filename,
		IsOpen:  true,
		IsInput: false,
	}

	// Call the procedure with the port as argument
	result, err := applyObjectProcedure(args[1], []obj.Object{port}, env)
	if err != nil {
		// Close the port before returning the error
		port.IsOpen = false
		return &Error{Message: err.Error()}
	}

	// Close the port
	port.IsOpen = false

	return result
}

// WithInputFromFile implements (with-input-from-file string thunk)
func WithInputFromFile(args []obj.Object, env *obj.Environment) obj.Object {
	if len(args) != 2 {
		return &Error{Message: "with-input-from-file requires exactly 2 arguments"}
	}

	if args[0].Type() != obj.STRING_OBJ {
		return &Error{Message: "with-input-from-file: first argument must be a string"}
	}

	// Check if the second argument is a procedure (thunk)
	procType := args[1].Type()
	if procType != obj.PROCEDURE_OBJ && procType != obj.BUILTIN_OBJ && procType != obj.LAMBDA_OBJ {
		return &Error{Message: "with-input-from-file: second argument must be a procedure"}
	}

	_ = args[0].(*obj.StringObject).Value // Use the filename variable

	// In a full implementation, we would:
	// 1. Save the current input port
	// 2. Open the new input file and make it the current input port
	// 3. Call the thunk with no arguments
	// 4. Restore the original input port
	// 5. Close the file

	// For now, we'll just call the thunk and return its result
	result, err := applyObjectProcedure(args[1], []obj.Object{}, env)
	if err != nil {
		return &Error{Message: err.Error()}
	}
	return result
}

// WithOutputToFile implements (with-output-to-file string thunk)
func WithOutputToFile(args []obj.Object, env *obj.Environment) obj.Object {
	if len(args) != 2 {
		return &Error{Message: "with-output-to-file requires exactly 2 arguments"}
	}

	if args[0].Type() != obj.STRING_OBJ {
		return &Error{Message: "with-output-to-file: first argument must be a string"}
	}

	// Check if the second argument is a procedure (thunk)
	procType := args[1].Type()
	if procType != obj.PROCEDURE_OBJ && procType != obj.BUILTIN_OBJ && procType != obj.LAMBDA_OBJ {
		return &Error{Message: "with-output-to-file: second argument must be a procedure"}
	}

	_ = args[0].(*obj.StringObject).Value // Use the filename variable

	// In a full implementation, we would:
	// 1. Save the current output port
	// 2. Open the new output file and make it the current output port
	// 3. Call the thunk with no arguments
	// 4. Restore the original output port
	// 5. Close the file

	// For now, we'll just call the thunk and return its result
	result, err := applyObjectProcedure(args[1], []obj.Object{}, env)
	if err != nil {
		return &Error{Message: err.Error()}
	}
	return result
}

// Read implements (read) and (read port)
func Read(args []obj.Object, env *obj.Environment) obj.Object {
	// According to R4RS, read takes an optional port argument
	if len(args) > 1 {
		return &Error{Message: "read takes at most 1 argument"}
	}

	// Check if a port is provided and validate it
	if len(args) == 1 {
		if args[0].Type() != obj.PORT_OBJ {
			return &Error{Message: "read: port argument must be a port object"}
		}

		port := args[0].(*obj.PortObject)
		if !port.IsOpen {
			return &Error{Message: "read: port is not open"}
		}

		if !port.IsInput {
			return &Error{Message: "read: port is not an input port"}
		}

		// In a full implementation, we would extract the actual file from the port
		// and read the next expression from it
		// For now, we'll just return NULL_OBJECT since we don't have real file I/O
		return obj.NULL_OBJECT
	}

	// When no port is provided, according to R4RS, we should read from the current input port
	// For now, we'll just return NULL_OBJECT since we don't have real file I/O
	return obj.NULL_OBJECT
}

// ReadChar implements (read-char) and (read-char port)
func ReadChar(args []obj.Object, env *obj.Environment) obj.Object {
	// According to R4RS, read-char takes an optional port argument
	if len(args) > 1 {
		return &Error{Message: "read-char takes at most 1 argument"}
	}

	// Check if a port is provided and validate it
	if len(args) == 1 {
		if args[0].Type() != obj.PORT_OBJ {
			return &Error{Message: "read-char: port argument must be a port object"}
		}

		port := args[0].(*obj.PortObject)
		if !port.IsOpen {
			return &Error{Message: "read-char: port is not open"}
		}

		if !port.IsInput {
			return &Error{Message: "read-char: port is not an input port"}
		}
	}

	// In a full implementation:
	// 1. Get the port (default to current input port if none provided)
	// 2. Read the next character from the port
	// 3. Return the character or EOF object

	// For now, just return a dummy character
	// In a real implementation, we would check if we've reached EOF and return obj.EOF_OBJECT if so
	return &obj.CharacterObject{Value: 'a'}
}

// PeekChar implements (peek-char) and (peek-char port)
func PeekChar(args []obj.Object, env *obj.Environment) obj.Object {
	// According to R4RS, peek-char takes an optional port argument
	if len(args) > 1 {
		return &Error{Message: "peek-char takes at most 1 argument"}
	}

	// Check if a port is provided and validate it
	if len(args) == 1 {
		if args[0].Type() != obj.PORT_OBJ {
			return &Error{Message: "peek-char: port argument must be a port object"}
		}

		port := args[0].(*obj.PortObject)
		if !port.IsOpen {
			return &Error{Message: "peek-char: port is not open"}
		}

		if !port.IsInput {
			return &Error{Message: "peek-char: port is not an input port"}
		}
	}

	// In a full implementation:
	// 1. Get the port (default to current input port if none provided)
	// 2. Peek at the next character from the port without consuming it
	// 3. Return the character or EOF object

	// For now, just return a dummy character
	// In a real implementation, we would check if we've reached EOF and return obj.EOF_OBJECT if so
	return &obj.CharacterObject{Value: 'a'}
}

// Write implements (write obj) and (write obj port)
func Write(args []obj.Object, env *obj.Environment) obj.Object {
	// According to R4RS, write takes an object and an optional port
	if len(args) < 1 || len(args) > 2 {
		return &Error{Message: "write takes 1 or 2 arguments"}
	}

	// Check if a port is provided and validate it
	if len(args) == 2 {
		if args[1].Type() != obj.PORT_OBJ {
			return &Error{Message: "write: port argument must be a port object"}
		}

		port := args[1].(*obj.PortObject)
		if !port.IsOpen {
			return &Error{Message: "write: port is not open"}
		}

		if port.IsInput {
			return &Error{Message: "write: port is not an output port"}
		}
	}

	// In a full implementation:
	// 1. Get the object to write (args[0])
	// 2. Get the port (default to current output port if none provided)
	// 3. Write a machine-readable representation of the object to the port

	// For now, just return NULL_OBJECT
	return obj.NULL_OBJECT
}

// Display implements (display obj) and (display obj port)
func Display(args []obj.Object, env *obj.Environment) obj.Object {
	// According to R4RS, display takes an object and an optional port
	if len(args) < 1 || len(args) > 2 {
		return &Error{Message: "display takes 1 or 2 arguments"}
	}

	// Check if a port is provided and validate it
	if len(args) == 2 {
		if args[1].Type() != obj.PORT_OBJ {
			return &Error{Message: "display: port argument must be a port object"}
		}

		port := args[1].(*obj.PortObject)
		if !port.IsOpen {
			return &Error{Message: "display: port is not open"}
		}

		if port.IsInput {
			return &Error{Message: "display: port is not an output port"}
		}
	}

	objToDisplay := args[0]

	// For now, we only support displaying to standard output
	// In a full implementation, we would check if a port is provided (args[1])
	// and verify it's a valid output port

	// Print the object's string representation to standard output
	// For display, we print the "human-readable" representation
	switch objToDisplay.Type() {
	case obj.STRING_OBJ:
		// For strings, display prints the string content without quotes
		str := objToDisplay.(*obj.StringObject)
		fmt.Print(str.Value)
	case obj.CHARACTER_OBJ:
		// For characters, display prints the character itself (not #\x format)
		char := objToDisplay.(*obj.CharacterObject)
		fmt.Print(string(char.Value))
	default:
		// For other objects, use their standard Inspect representation
		fmt.Print(objToDisplay.Inspect())
	}

	return obj.NULL_OBJECT
}

// Newline implements (newline) and (newline port)
func Newline(args []obj.Object, env *obj.Environment) obj.Object {
	// According to R4RS, newline takes an optional port argument
	if len(args) > 1 {
		return &Error{Message: "newline takes at most 1 argument"}
	}

	// Check if a port is provided and validate it
	if len(args) == 1 {
		if args[0].Type() != obj.PORT_OBJ {
			return &Error{Message: "newline: port argument must be a port object"}
		}

		port := args[0].(*obj.PortObject)
		if !port.IsOpen {
			return &Error{Message: "newline: port is not open"}
		}

		if port.IsInput {
			return &Error{Message: "newline: port is not an output port"}
		}
	}

	// In a full implementation:
	// 1. Get the port (default to current output port if none provided)
	// 2. Write a newline character to the port

	fmt.Println() // Print a newline to standard output

	return obj.NULL_OBJECT
}

// Load implements (load filename)
func Load(args []obj.Object, env *obj.Environment) obj.Object {
	if len(args) != 1 {
		return &Error{Message: "load requires exactly 1 argument"}
	}

	if args[0].Type() != obj.STRING_OBJ {
		return &Error{Message: "load argument must be a string"}
	}

	filename := args[0].(*obj.StringObject).Value

	// Open the file
	data, err := os.ReadFile(filename)
	if err != nil {
		if os.IsNotExist(err) {
			// Try to get current working directory for better error message
			if wd, err := os.Getwd(); err == nil {
				return &Error{Message: fmt.Sprintf("load: file %s does not exist (current directory: %s)", filename, wd)}
			}
			return &Error{Message: fmt.Sprintf("load: file %s does not exist", filename)}
		}
		return &Error{Message: fmt.Sprintf("load: error reading file %s: %v", filename, err)}
	}

	// Convert file contents to string
	return LoadData(string(data), filename, env)
}

func LoadData(input string, filename string, env *obj.Environment) obj.Object {
	// Create lexer
	l := lex.NewLexer(input)
	tokens := l.ReadTokens()

	// Check for lexical errors
	for _, token := range tokens {
		if token.Type == tok.ILLEGAL {
			return &Error{Message: fmt.Sprintf("load: lexical error in file %s at line %d column %d: %s", filename, token.Line, token.Column, token.Lexeme)}
		}
	}

	// Create parser
	p := parse.NewParser(tokens)

	// Parse expressions
	exprs, err := p.ParseProgram()
	if err != nil {
		return &Error{Message: fmt.Sprintf("load: parse error in file %s: %v", filename, err)}
	}

	// Evaluate each expression in the given environment
	var result obj.Object = obj.NULL_OBJECT
	for _, expr := range exprs {
		result, err = EvalExpression(expr, env)
		if err != nil {
			return &Error{Message: fmt.Sprintf("load: evaluation error in file %s: %v", filename, err)}
		}
	}

	// Return the value of the last expression
	return result
}

// TranscriptOn implements (transcript-on filename)
func TranscriptOn(args []obj.Object, env *obj.Environment) obj.Object {
	if len(args) != 1 {
		return &Error{Message: "transcript-on requires exactly 1 argument"}
	}

	if args[0].Type() != obj.STRING_OBJ {
		return &Error{Message: "transcript-on argument must be a string"}
	}

	if transcriptActive {
		return &Error{Message: "transcript is already active"}
	}

	filename := args[0].(*obj.StringObject).Value

	// Open the transcript file for writing (append mode)
	file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil {
		return &Error{Message: fmt.Sprintf("transcript-on: error opening file %s: %v", filename, err)}
	}

	transcriptFile = file
	transcriptActive = true

	return obj.NULL_OBJECT
}

// TranscriptOff implements (transcript-off)
func TranscriptOff(args []obj.Object, env *obj.Environment) obj.Object {
	if len(args) != 0 {
		return &Error{Message: "transcript-off takes no arguments"}
	}

	if !transcriptActive {
		return &Error{Message: "transcript is not active"}
	}

	// Close the transcript file
	if err := transcriptFile.Close(); err != nil {
		return &Error{Message: fmt.Sprintf("transcript-off: error closing transcript file: %v", err)}
	}

	transcriptFile = nil
	transcriptActive = false

	return obj.NULL_OBJECT
}

// WriteChar implements (write-char char) and (write-char char port)
func WriteChar(args []obj.Object, env *obj.Environment) obj.Object {
	// According to R4RS, write-char takes a character and an optional port
	if len(args) < 1 || len(args) > 2 {
		return &Error{Message: "write-char takes 1 or 2 arguments"}
	}

	if args[0].Type() != obj.CHARACTER_OBJ {
		return &Error{Message: "write-char requires a character as first argument"}
	}

	char := args[0].(*obj.CharacterObject)

	// Check if a port is provided and validate it
	if len(args) == 2 {
		if args[1].Type() != obj.PORT_OBJ {
			return &Error{Message: "write-char: port argument must be a port object"}
		}

		port := args[1].(*obj.PortObject)
		if !port.IsOpen {
			return &Error{Message: "write-char: port is not open"}
		}

		if port.IsInput {
			return &Error{Message: "write-char: port is not an output port"}
		}
	}

	// In a full implementation:
	// 1. Get the character to write (args[0])
	// 2. Get the port (default to current output port if none provided)
	// 3. Write the character to the port

	// For now, just print to standard output
	fmt.Print(string(char.Value))

	return obj.NULL_OBJECT
}
